import torch
import numpy as np
import random
import matplotlib
import matplotlib.pyplot as plt

# 设置后端
matplotlib.use('TkAgg')  # 或者 'Agg' 后端，这里设置图形显示方式为TkAgg，可以修改为其他后端

# 定义数据迭代器，按批次返回数据
def data_iter(batch_size, features, labels):
    num_examples = len(features)  # 获取样本数量
    indices = list(range(num_examples))  # 生成索引
    random.shuffle(indices)  # 随机打乱索引
    for i in range(0, num_examples, batch_size):  # 按批次处理数据
        j = torch.LongTensor(indices[i: min(i + batch_size, num_examples)])  # 获取当前批次的索引
        # 返回当前批次的特征和标签
        yield features.index_select(0, j), labels.index_select(0, j)

# 定义线性回归模型
def linreg(X, w, b):
    return torch.mm(X, w) + b  # 线性模型计算，X * w + b

# 定义平方损失函数（均方误差 MSE）
def squared_loss(y_hat, y): 
    # 返回每个样本的损失（加上 / 2，通常这样可以方便计算梯度）
    return (y_hat - y.view(y_hat.size())) ** 2 / 2

# 定义梯度下降优化算法
def sgd(params, lr, batch_size):
    # 对每个参数执行一次梯度下降更新
    for param in params:
        param.data -= lr * param.grad / batch_size  # 更新权重

# 设置图形的大小
def set_figsize(figsize=(3.5, 2.5)):
    plt.rcParams['figure.figsize'] = figsize  # 设置图形的默认大小

# 定义模型参数
num_inputs = 2  # 输入特征的维度
num_examples = 1000  # 样本数量
true_w = [2, -3.4]  # 真实的权重值
true_b = 4.2  # 真实的偏置值

# 生成特征数据，使用标准正态分布（均值0，标准差1）
features = torch.from_numpy(np.random.normal(0, 1, (num_examples, num_inputs))).float()
# 计算标签：y = true_w[0] * X1 + true_w[1] * X2 + true_b
labels = true_w[0] * features[:, 0] + true_w[1] * features[:, 1] + true_b
# 添加随机噪声
labels += torch.from_numpy(np.random.normal(0, 0.01, size=labels.size())).float()

# 可视化数据，绘制第二个特征与标签的关系图
set_figsize()
plt.scatter(features[:, 1].numpy(), labels.numpy(), 1)  # 使用第二列特征与标签进行绘图
plt.show()  # 显示图形

# 初始化模型参数（权重和偏置）
batch_size = 10  # 批量大小
w = torch.tensor(np.random.normal(0, 0.01, (num_inputs, 1)), dtype=torch.float32)  # 初始化权重
b = torch.zeros(1, dtype=torch.float32)  # 初始化偏置

# 设置需要计算梯度的参数
w.requires_grad_(requires_grad=True)
b.requires_grad_(requires_grad=True)

# 设置超参数
lr = 0.03  # 学习率
num_epochs = 5  # 迭代次数

# 训练模型
for epoch in range(num_epochs):
    for X, y in data_iter(batch_size, features, labels):  # 遍历每个批次
        l = squared_loss(linreg(X, w, b), y).sum()  # 计算损失
        l.backward()  # 计算损失的梯度
        sgd([w, b], lr, batch_size)  # 使用梯度下降更新模型参数
        w.grad.data.zero_()  # 清空权重的梯度
        b.grad.data.zero_()  # 清空偏置的梯度

    # 计算当前epoch的训练损失
    train_l = squared_loss(linreg(features, w, b), labels)
    print('epoch %d, loss %f' % (epoch + 1, train_l.mean().item()))  # 打印当前epoch的平均损失

# 输出训练结束后的结果，比较预测的权重和偏置与真实的值
print(true_w, '\n', w)
print(true_b, '\n', b)
